package island

import (
	"context"
	"errors"
	"github.com/Unixeno/islands/island/log"
	"github.com/Unixeno/islands/pb"
	"github.com/Unixeno/islands/stringid"
	"github.com/containerd/console"
	"github.com/containerd/go-runc"
	"github.com/opencontainers/runtime-spec/specs-go"
	"io"
	"os"
	"path/filepath"
	"sync"
)

var execinfo *ExecInfo
var execPath = filepath.Join(filepath.Dir(containerRootDir), "exec")

func GetExecInfo() *ExecInfo {
	return execinfo
}

func CreateExecInstance(options ProcessOptions) (string, error) {
	//	TODO: 检测容器存活状态

	execid := stringid.GenerateNonCryptoID()

	execInstancePath := filepath.Join(execPath, execid)

	if !PathExist(execInstancePath) {
		if err := os.MkdirAll(execInstancePath, 0755); err != nil {
			logger.WithField("exec-id", execid).WithField("error", err).Error("cannot make dirs")
		}
	}

	options.Env = append(options.Env, "PATH=$PATH:/usr/bin:/bin:/sbin")

	streamLogger, err := log.NewStreamLogger(execInstancePath)
	if err != nil {
		logger.WithField("exec-id", execid).WithField("error", err).Error("cannot get stream logger")
	}

	exec := &execInstance{
		ExecId:      execid,
		logs:        streamLogger,
		ContainerId: options.ContainerId,
		Stdin:       options.Stdin,
		Stdout:      options.Stdout,
		Stderr:      options.Stderr,
		Process:     NewProcess(options),
		consoleLock: sync.Mutex{},
	}

	if c, err := Open(options.ContainerId); err != nil {
		logger.WithField("exec-id", execid).WithField("error", err).Error("cannot get stream logger")
		return "", err
	} else {
		c.SetExecInstance(exec)
	}

	execinfo.ExecLock.Lock()
	execinfo.ExecInstances[execid] = exec
	execinfo.ExecLock.Unlock()

	return execid, nil
}

func GetExecInstance(execid string) (*execInstance, error) {
	execinfo.ExecLock.RLock()
	defer execinfo.ExecLock.RUnlock()
	if instance, ok := execinfo.ExecInstances[execid]; !ok {
		return nil, errors.New("exec instance not exist")
	} else {
		return instance, nil
	}
}

func (e *execInstance)SetExecRunningState(running bool) {
	e.Running = running
}

func (e *execInstance) StartExecInstance() error {
	var consoleSocket *runc.Socket = nil

	if e.Process.Terminal {
		var err error
		consoleSocket, err = runc.NewTempConsoleSocket()
		if err != nil {
			logger.WithField("exec-id", e.ExecId).WithError(err).Error("failed to generate temp console socket")
			return err
		}
	}

	IO, err := runc.NewPipeIO(os.Getuid(), os.Getgid())
	if err != nil {
		logger.WithField("exec-id", e.ExecId).WithError(err).Error("failed to generate pipes")
		return nil
	}

	var opts *runc.ExecOpts
	if e.Process.Terminal {
		opts = &runc.ExecOpts{
			IO:            IO,
			PidFile:       filepath.Join(execPath, e.ExecId, "pid.log"),
			ConsoleSocket: consoleSocket,
			Detach:        true,
		}
	} else {
		opts = &runc.ExecOpts{
			IO:      IO,
			PidFile: filepath.Join(execPath, e.ExecId, "pid.log"),
			Detach:  true,
		}
	}

	// 可能存在console还没有设置成功,但是resizetty请求已经发送到的情况,所以这里加锁保证顺序
	if e.Process.Terminal {
		e.consoleLock.Lock()
	}
    
	startProcessErr := e.Process.StartProcess(func() (int, error) {
		runcErr := tool.Exec(context.Background(), e.ContainerId, e.Process.Process, opts)
		if runcErr != nil {
			return -1, runcErrHandle(IO, e.ExecId)
		}
		e.Running = true
		pid, getPidErr := runc.ReadPidFile(filepath.Join(execPath, e.ExecId, "pid.log"))
		return pid, getPidErr
	})

	if startProcessErr != nil {
		logger.WithField("id", e.ExecId).WithField("error", startProcessErr).Error("process start failed")
		return startProcessErr
	}

	if !e.Process.Terminal {
		e.Process.StdinWriter = IO.Stdin()
		e.Process.StdoutReader = IO.Stdout()
		e.Process.StderrReader = IO.Stderr()
	} else {
		masterEnd, err := consoleSocket.ReceiveMaster()
		if err != nil {
			logger.WithField("id", e.ExecId).WithError(err).Error("failed to receive pty master")
			return err
		}
		e.console = masterEnd
		e.consoleLock.Unlock()

		e.Process.StdinWriter = masterEnd
		e.Process.StdoutReader = masterEnd
		e.Process.StderrReader = masterEnd
	}


	execinfo.ExecLock.Lock()
	execinfo.ExecInstances[e.ExecId] = e
	execinfo.ExecLock.Unlock()

	if e.Stdout {
		go e.AttachExecStdout(e.grpcIO)
	}

	if e.Stdin {
		go e.AttachExecStdin(e.grpcIO)
	}

	// 开启日志
	e.logs.Start()
	e.SetLog()

	return nil
}

func (e *execInstance) ResizeExecConsole(box console.WinSize) {
	// 想要resize的话,先获得consoleLock,防止出现console未设置的情况
	e.consoleLock.Lock()
	defer e.consoleLock.Unlock()
	if e.console != nil {
		if err := e.console.Resize(box); err != nil {
			logger.WithField("id", e.ExecId).Error("cannot resize exec console")
		}
	} else {
		e.Process.ConsoleSize = &specs.Box{
			Width:  uint(box.Width),
			Height: uint(box.Height),
		}
	}
}

func (e *execInstance) AttachExecStdin(grpcIO *GrpcServerIO) {
	logger.Debug("attach exec in")
	_, err := io.Copy(e.Process.StdinWriter, grpcIO)
	if err != nil {
		logger.WithField("id", grpcIO.id).Debug("close the stdin")
	}
	close(e.Process.StdinChan)
}

func (e *execInstance) SetLog() {
	e.grpcIO.logger = e.logs
}

func (e *execInstance) AttachExecStdout(grpcIO *GrpcServerIO) {
	logger.Debug("attach exec out")
	_, err := io.Copy(grpcIO, e.Process.StdoutReader)
	if err != nil {
		logger.WithField("id", grpcIO.id).Debug("close the stdout")
	}
	close(e.Process.StdoutChan)
}

func (e *execInstance) SetGrpcIO(grpcIO *GrpcServerIO) {
	e.grpcIO = grpcIO
}

func NewGrpcIO(id string, stream AttachServerStream) *GrpcServerIO {
	return &GrpcServerIO{
		stream: stream,
		id:     id,
	}
}

func (g GrpcServerIO) Write(content []byte) (n int, err error) {
	err = g.stream.Send(&pb.AttachStreamOut{
		Content: content,
	})

	// 是否开启日志收集
	if g.logger != nil{
		_, _ = g.logger.Stdout.Write(content)
	}

	n = len(content)
	return
}

func (g GrpcServerIO) Read(content []byte) (n int, err error) {

	check := func(grpcIO GrpcServerIO, resp *pb.AttachStreamIn, copyNum int) {
		if copyNum != len(resp.Content) {
			g.remainNum = len(resp.Content) - copyNum
			g.remainBytes = resp.Content[g.remainNum:]
		}
	}

	if g.remainNum == 0 && len(g.remainBytes) == 0 {
		r, err := g.stream.Recv()

		if err != nil {
			if r != nil {
				if len(r.Content) != 0 {
					copyNum := copy(content, r.Content)
					check(g, r, copyNum)
					return copyNum, err
				}
			}
			return 0, err
		} else {

			// 是否开启日志收集
			if g.logger != nil{
				_, _ = g.logger.Stdin.Write(r.Content)
			}

			copyNum := copy(content, r.Content)
			check(g, r, copyNum)
			return len(r.Content), err
		}
	} else {
		copyNum := copy(content, g.remainBytes)
		if copyNum != g.remainNum {
			g.remainNum = g.remainNum - copyNum
			g.remainBytes = g.remainBytes[g.remainNum:]
		}
		return copyNum, err
	}
}

func (e *execInstance) GetLogs() *log.StreamLogger {
	return e.logs
}